/**
 * NESDroid
 * Copyright 2011 Stephen Damm (Halsafar)
 * All rights reserved.
 * shinhalsafar@gmail.com
 */

package ca.halsafar.nesdroid;

import android.app.Activity;
import android.content.Context;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.opengl.GLSurfaceView;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Window;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.opengles.GL10;

import ca.halsafar.audio.AudioPlayer;


/**
 * EmulatorView, the OpenGL rendering context and thread.
 * Calls the emulation step in native code
 * Mixes audio samples to AudioTrack
 * Performs vsync 
 * 
 * @author halsafar
 * 
 */
class EmulatorView extends GLSurfaceView
{
     private static String        LOG_TAG          = "EmulatorView";
     private static final boolean DEBUG            = false;

     public static int            TitleBarHeight   = 0;
     public static int            MenuBarHeight    = 0;
     public static int            TotalOuterHeight = 0;

     protected static boolean     _hasFocus;

     private static Activity      _activity;


     public EmulatorView(Activity activity, Context context)
     {
          super(context);

          _activity = activity;

          init(false, 0, 0);

          // setDebugFlags(DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS);
          setDebugFlags(0);

          setFocusableInTouchMode(true);
          requestFocus();
     }


     public EmulatorView(Activity activity, Context context,
               boolean translucent, int depth, int stencil)
     {
          super(context);

          _activity = activity;

          init(translucent, depth, stencil);

          // setDebugFlags(DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS);
          setDebugFlags(0);

          setFocusableInTouchMode(true);
          requestFocus();
     }


     private void init(boolean translucent, int depth, int stencil)
     {

          /*
           * By default, GLSurfaceView() creates a RGB_565 opaque surface. If we
           * want a translucent one, we should change the surface's format here,
           * using PixelFormat.TRANSLUCENT for GL Surfaces is interpreted as any
           * 32-bit surface with alpha by SurfaceFlinger.
           */
          if (translucent)
          {
               this.getHolder().setFormat(PixelFormat.TRANSLUCENT);
          }

          /*
           * Setup the context factory for 2.0 rendering. See ContextFactory
           * class definition below
           */
          setEGLContextFactory(new ContextFactory());

          /*
           * We need to choose an EGLConfig that matches the format of our
           * surface exactly. This is going to be done in our custom config
           * chooser. See ConfigChooser class definition below.
           */
          setEGLConfigChooser(translucent ?
                             new ConfigChooser(8, 8, 8, 8, depth, stencil) :
                             new ConfigChooser(5, 6, 5, 0, depth, stencil));

          /* Set the renderer responsible for frame rendering */
          setRenderer(new Renderer());
          // setRenderMode(RENDERMODE_WHEN_DIRTY);

          setKeepScreenOn(true);
          setClickable(false);
     }

     private static class ContextFactory implements
               GLSurfaceView.EGLContextFactory
     {
          private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;


          public EGLContext createContext(EGL10 egl, EGLDisplay display,
                    EGLConfig eglConfig)
          {
               Log.w(LOG_TAG, "creating OpenGL ES 2.0 context");
               checkEglError("Before eglCreateContext", egl);
               int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2,
                         EGL10.EGL_NONE };
               EGLContext context = egl.eglCreateContext(display, eglConfig,
                         EGL10.EGL_NO_CONTEXT, attrib_list);
               checkEglError("After eglCreateContext", egl);
               return context;
          }


          public void destroyContext(EGL10 egl, EGLDisplay display,
                    EGLContext context)
          {
               egl.eglDestroyContext(display, context);
          }
     }


     private static void checkEglError(String prompt, EGL10 egl)
     {
          int error;
          while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS)
          {
               Log.e(LOG_TAG, String.format("%s: EGL error: 0x%x", prompt,
                         error));
          }
     }

     private static class ConfigChooser implements
               GLSurfaceView.EGLConfigChooser
     {

          public ConfigChooser(int r, int g, int b, int a, int depth,
                    int stencil)
          {
               mRedSize = r;
               mGreenSize = g;
               mBlueSize = b;
               mAlphaSize = a;
               mDepthSize = depth;
               mStencilSize = stencil;
          }

          /*
           * This EGL config specification is used to specify 2.0 rendering. We
           * use a minimum size of 4 bits for red/green/blue, but will perform
           * actual matching in chooseConfig() below.
           */
          private static int   EGL_OPENGL_ES2_BIT = 4;
          private static int[] s_configAttribs2   =
                                                  {
                                                            EGL10.EGL_RED_SIZE,
                                                            4,
                                                            EGL10.EGL_GREEN_SIZE,
                                                            4,
                                                            EGL10.EGL_BLUE_SIZE,
                                                            4,
                                                            EGL10.EGL_RENDERABLE_TYPE,
                                                            EGL_OPENGL_ES2_BIT,
                                                            EGL10.EGL_NONE
                                                  };


          public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display)
          {

               /*
                * Get the number of minimally matching EGL configurations
                */
               int[] num_config = new int[1];
               egl.eglChooseConfig(display, s_configAttribs2, null, 0,
                         num_config);

               int numConfigs = num_config[0];

               if (numConfigs <= 0)
               {
                    throw new IllegalArgumentException(
                              "No configs match configSpec");
               }

               /*
                * Allocate then read the array of minimally matching EGL configs
                */
               EGLConfig[] configs = new EGLConfig[numConfigs];
               egl.eglChooseConfig(display, s_configAttribs2, configs,
                         numConfigs, num_config);

               if (DEBUG)
               {
                    printConfigs(egl, display, configs);
               }
               /*
                * Now return the "best" one
                */
               return chooseConfig(egl, display, configs);
          }


          public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
                    EGLConfig[] configs)
          {
               for (EGLConfig config : configs)
               {
                    int d = findConfigAttrib(egl, display, config,
                              EGL10.EGL_DEPTH_SIZE, 0);
                    int s = findConfigAttrib(egl, display, config,
                              EGL10.EGL_STENCIL_SIZE, 0);

                    // We need at least mDepthSize and mStencilSize bits
                    if (d < mDepthSize || s < mStencilSize)
                         continue;

                    // We want an *exact* match for red/green/blue/alpha
                    int r = findConfigAttrib(egl, display, config,
                              EGL10.EGL_RED_SIZE, 0);
                    int g = findConfigAttrib(egl, display, config,
                              EGL10.EGL_GREEN_SIZE, 0);
                    int b = findConfigAttrib(egl, display, config,
                              EGL10.EGL_BLUE_SIZE, 0);
                    int a = findConfigAttrib(egl, display, config,
                              EGL10.EGL_ALPHA_SIZE, 0);

                    if (r == mRedSize && g == mGreenSize && b == mBlueSize
                              && a == mAlphaSize)
                         return config;
               }
               return null;
          }


          private int findConfigAttrib(EGL10 egl, EGLDisplay display,
                    EGLConfig config, int attribute, int defaultValue)
          {

               if (egl.eglGetConfigAttrib(display, config, attribute, mValue))
               {
                    return mValue[0];
               }
               return defaultValue;
          }


          private void printConfigs(EGL10 egl, EGLDisplay display,
                    EGLConfig[] configs)
          {
               int numConfigs = configs.length;
               Log.w(LOG_TAG, String.format("%d configurations", numConfigs));
               for (int i = 0; i < numConfigs; i++)
               {
                    Log.w(LOG_TAG, String.format("Configuration %d:\n", i));
                    printConfig(egl, display, configs[i]);
               }
          }


          private void printConfig(EGL10 egl, EGLDisplay display,
                    EGLConfig config)
          {
               int[] attributes = {
                         EGL10.EGL_BUFFER_SIZE,
                         EGL10.EGL_ALPHA_SIZE,
                         EGL10.EGL_BLUE_SIZE,
                         EGL10.EGL_GREEN_SIZE,
                         EGL10.EGL_RED_SIZE,
                         EGL10.EGL_DEPTH_SIZE,
                         EGL10.EGL_STENCIL_SIZE,
                         EGL10.EGL_CONFIG_CAVEAT,
                         EGL10.EGL_CONFIG_ID,
                         EGL10.EGL_LEVEL,
                         EGL10.EGL_MAX_PBUFFER_HEIGHT,
                         EGL10.EGL_MAX_PBUFFER_PIXELS,
                         EGL10.EGL_MAX_PBUFFER_WIDTH,
                         EGL10.EGL_NATIVE_RENDERABLE,
                         EGL10.EGL_NATIVE_VISUAL_ID,
                         EGL10.EGL_NATIVE_VISUAL_TYPE,
                         0x3030, // EGL10.EGL_PRESERVED_RESOURCES,
                         EGL10.EGL_SAMPLES,
                         EGL10.EGL_SAMPLE_BUFFERS,
                         EGL10.EGL_SURFACE_TYPE,
                         EGL10.EGL_TRANSPARENT_TYPE,
                         EGL10.EGL_TRANSPARENT_RED_VALUE,
                         EGL10.EGL_TRANSPARENT_GREEN_VALUE,
                         EGL10.EGL_TRANSPARENT_BLUE_VALUE,
                         0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB,
                         0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA,
                         0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL,
                         0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL,
                         EGL10.EGL_LUMINANCE_SIZE,
                         EGL10.EGL_ALPHA_MASK_SIZE,
                         EGL10.EGL_COLOR_BUFFER_TYPE,
                         EGL10.EGL_RENDERABLE_TYPE,
                         0x3042 // EGL10.EGL_CONFORMANT
               };
               String[] names = {
                         "EGL_BUFFER_SIZE",
                         "EGL_ALPHA_SIZE",
                         "EGL_BLUE_SIZE",
                         "EGL_GREEN_SIZE",
                         "EGL_RED_SIZE",
                         "EGL_DEPTH_SIZE",
                         "EGL_STENCIL_SIZE",
                         "EGL_CONFIG_CAVEAT",
                         "EGL_CONFIG_ID",
                         "EGL_LEVEL",
                         "EGL_MAX_PBUFFER_HEIGHT",
                         "EGL_MAX_PBUFFER_PIXELS",
                         "EGL_MAX_PBUFFER_WIDTH",
                         "EGL_NATIVE_RENDERABLE",
                         "EGL_NATIVE_VISUAL_ID",
                         "EGL_NATIVE_VISUAL_TYPE",
                         "EGL_PRESERVED_RESOURCES",
                         "EGL_SAMPLES",
                         "EGL_SAMPLE_BUFFERS",
                         "EGL_SURFACE_TYPE",
                         "EGL_TRANSPARENT_TYPE",
                         "EGL_TRANSPARENT_RED_VALUE",
                         "EGL_TRANSPARENT_GREEN_VALUE",
                         "EGL_TRANSPARENT_BLUE_VALUE",
                         "EGL_BIND_TO_TEXTURE_RGB",
                         "EGL_BIND_TO_TEXTURE_RGBA",
                         "EGL_MIN_SWAP_INTERVAL",
                         "EGL_MAX_SWAP_INTERVAL",
                         "EGL_LUMINANCE_SIZE",
                         "EGL_ALPHA_MASK_SIZE",
                         "EGL_COLOR_BUFFER_TYPE",
                         "EGL_RENDERABLE_TYPE",
                         "EGL_CONFORMANT"
               };
               int[] value = new int[1];
               for (int i = 0; i < attributes.length; i++)
               {
                    int attribute = attributes[i];
                    String name = names[i];
                    if (egl.eglGetConfigAttrib(display, config, attribute,
                              value))
                    {                         
                         Log.w(LOG_TAG, String.format("  %s: %d\n", name,
                                   value[0]));
                    }
                    else
                    {
                         // Log.w(TAG, String.format("  %s: failed\n", name));
                         while (egl.eglGetError() != EGL10.EGL_SUCCESS)
                              ;
                    }
               }
          }

          // Subclasses can adjust these values:
          protected int mRedSize;
          protected int mGreenSize;
          protected int mBlueSize;
          protected int mAlphaSize;
          protected int mDepthSize;
          protected int mStencilSize;
          private int[] mValue = new int[1];
     }


     @Override
     public boolean dispatchTouchEvent(final MotionEvent event)
     {
          // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY);
          // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
          // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
          int actionMasked = event.getActionMasked();
          if (actionMasked == MotionEvent.ACTION_POINTER_DOWN
                        || actionMasked == MotionEvent.ACTION_DOWN)
          {
               int pointerIndex = event.getActionIndex();
               Emulator.onTouchDown(event.getPointerId(pointerIndex),
                         event.getX(pointerIndex),
                         event.getY(pointerIndex)
                                   - EmulatorView.TotalOuterHeight,
                         event.getSize(pointerIndex));
          }
          else if (actionMasked == MotionEvent.ACTION_POINTER_UP
                        || actionMasked == MotionEvent.ACTION_UP
                        || actionMasked == MotionEvent.ACTION_CANCEL)
          {
               int pointerIndex = event.getActionIndex();
               Emulator.onTouchUp(event.getPointerId(pointerIndex),
                         event.getX(pointerIndex),
                         event.getY(pointerIndex)
                                   - EmulatorView.TotalOuterHeight,
                         event.getSize(pointerIndex));
          }
          else if (actionMasked == MotionEvent.ACTION_MOVE)
          {
               final int count = event.getPointerCount();               
               for (int pointerIndex = 0; pointerIndex < count; pointerIndex++)
               {
                    Emulator.onTouchMove(event.getPointerId(pointerIndex),
                              (int) event.getX(pointerIndex),
                              (int) event.getY(pointerIndex)
                                        - EmulatorView.TotalOuterHeight,
                              event.getSize(pointerIndex));
               }
          }

          return true;
     }
     
    
     public boolean dispatchKeyEvent(KeyEvent event)
     {
          if (event.getAction() == KeyEvent.ACTION_DOWN)
          {
               Emulator.onKeyDown(event.getKeyCode());
          }
          else if (event.getAction() == KeyEvent.ACTION_UP)
          {
               Emulator.onKeyUp(event.getKeyCode());
          }
          
          return super.dispatchKeyEvent(event);
     }
     
    /* @Override
     public boolean onKeyUp(int keyCode, KeyEvent event)
     {
          Emulator.onKeyUp(event.getKeyCode());
          
          return super.onKeyUp(keyCode, event);
     }
     
     
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event)
     {
          Emulator.onKeyDown(event.getKeyCode());
          
          return super.onKeyDown(keyCode, event);
     }    */ 


     @Override
     public void onWindowFocusChanged(boolean hasFocus)
     {
          // Log.d(LOG_TAG, "onWindowFocusChanged(" + hasFocus + ")");

          _hasFocus = hasFocus;
     }

     private class Renderer implements GLSurfaceView.Renderer
     {
          short[] tempBuf = new short[1024];


          public void onDrawFrame(GL10 gl)
          {
               // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
               // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY);
               // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_FOREGROUND);
               // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_MORE_FAVORABLE);
               // android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY);

               if (_hasFocus)
               {
                    Emulator.step();

                    int readAmount = Emulator.mixAudioBuffer(tempBuf, 0, -1);
                    if (readAmount > 0)
                    {
                         //Log.d(LOG_TAG, "AudioMix Samples: " + readAmount);
                         AudioPlayer.play(tempBuf, readAmount);
                    }
               }

               Emulator.draw();                              
          }


          public void onSurfaceChanged(GL10 gl, int width, int height)
          {
               Log.d(LOG_TAG, "onSurfaceChanged()");
               
               Emulator.setViewport(width, height);
               /*SharedPreferences prefs = PreferenceManager
                         .getDefaultSharedPreferences(_activity
                                   .getApplicationContext());
               boolean keepAspect = prefs.getBoolean("aspectRatio", true);

               Emulator.setKeepAspectRatio(keepAspect);*/
          }


          public void onSurfaceCreated(GL10 gl, EGLConfig config)
          {
               Log.d(LOG_TAG, "onSurfaceCreated()");              
 
               // init graphics
               Emulator.initGraphics();
               
               // load prefs
               PreferenceFacade.loadGraphicsPrefs(EmulatorView.this.getContext());
               
               // calculate height of anything on the phone other than the game window
               // mostly for tablets!
               Rect rect = new Rect();
               Window window = _activity.getWindow();
               window.getDecorView().getWindowVisibleDisplayFrame(rect);
               int MenuBarHeight = rect.top;
               int contentViewTop = window.findViewById(
                         Window.ID_ANDROID_CONTENT)
                         .getTop();
               int TitleBarHeight = contentViewTop - MenuBarHeight;
               TotalOuterHeight = MenuBarHeight + TitleBarHeight;
          }
     }
}
